相信使用 ES6 的小夥伴們對模組都不陌生,現代前端框架也都使用模組在運作。而模組和命名空間是兩種組織和封裝程式碼的方式,雖然它們都有類似的目標,但它們在某些方面有不同的特性和用途。
TypeScript 提供了不同種類的模組系統,包括 CommonJS、ES6、AMD 和 UMD 等。我們可以根據您的項目需求選擇最合適的模組系統,但通常前端開發還是建議使用 ES6 模組。以下將以 ES6 模組來說明:
模組允許將相關的變數、函數、類型和接口等封裝在一個單獨的檔案中,這有助於減少全局命名衝突,同時提供了導出 ( export )和導入 ( import )功能,提高程式碼的重用性、維護性和模組化。
封裝性
: 模組允許將相關的代碼封裝在一個單獨的檔案中,並將其暴露給外部只有有限的訪問權限。
可重用性
: 模組可以在不同的檔案中定義,並且可以在需要時進行導入和重複使用。
以下是使用 ES6 模組的範例:
// module.ts
// 導出
export const greet = "Hello, TypeScript!";
export const sayHello = (name: string): void => {
console.log(`哈囉,${name}!`);
};
// app.ts
// 導入並使用模組
import { greet, sayHello } from "./module.js";
console.log(greet); // 輸出: Hello, TypeScript!
sayHello("威爾豬"); // 輸出: 哈囉,威爾豬!
在這個範例中,我們在 module.ts 檔案中使用 export 導出變數 greet 和 sayHello 函式。然後,我們在 app.ts 中使用 import 來導入這些模組。
注意: 為了方便演示,範例這邊導入模組最後加上 .js 副檔名,是為了讓編譯後的 app.js 自動帶入 .js 副檔名,不然會找不到檔案。( 原則上開發是不應該加上副檔名的。)
再看另一個例子,假設我們正在開發一個簡單的購物車系統,我們需要將商品和購物車分為不同的模組:
// product.ts
export interface IProduct {
id: number;
name: string;
price: number;
}
// cart.ts
import { IProduct } from "./product.js";
export class ShoppingCart {
private items: IProduct[] = [];
addItem(item: IProduct) {
this.items.push(item);
}
getItems(): IProduct[] {
return this.items;
}
}
我們 app.ts 中使用這些模組:
// app.ts
// 導入模組
import { IProduct } from "./product.js";
import { ShoppingCart } from "./cart.js";
const apple: IProduct = { id: 1, name: "Apple", price: 44900 };
const samsung: IProduct = { id: 2, name: "Samsung", price: 43900 };
const cart = new ShoppingCart();
cart.addItem(apple);
cart.addItem(samsung);
const items = cart.getItems();
console.log("購物車內容:", items); //輸出: 購物車內容:[{id: 1, name: "Apple", price: 44900}, {id: 2, name: "Samsung", price: 43900}]
從這個範例我們可以看到,模組讓程式碼更有組織性,並且可以在不同的檔案中分開定義和導入功能。
使用方式:namespace 名稱 {}
引入文件依順序建立相依性: /// <reference path="檔案路徑" />
命名空間提供了一種將相關功能和類別組織在一起的方法,以 避免在全域中發生命名衝突
的問題。不過命名空間通常主要在具有 較舊的模組系統或非模組的 JavaScript 環境中使用
。
看以下範例:
我們創建一個名為 Shapes 的命名空間,其中包含了 Circle 和 Rect 兩個類別,並在 app.ts 中使用 Shapes 裡的 Circle 和 Rect 類別。
// shapes.ts
namespace Shapes {
abstract class Shape {
constructor(protected a: number, protected b?: number) {}
}
export class Circle extends Shape {
calculateArea(): number {
return Math.round(Math.PI * this.a ** 2);
}
}
export class Rect extends Shape {
calculateArea(): number {
return this.a * this.b;
}
}
}
// app.ts
// 建立相依性
/// <reference path="shapes.ts" />
const circle = new Shapes.Circle(10);
const rect = new Shapes.Rect(4, 6);
console.log("圓形面積:", circle.calculateArea()); // 輸出: 圓形面積: 314
console.log("方形面積:", rect.calculateArea()); // 輸出: 方形面積: 24
當然我們也可以修改成使用別名的方式:
// app.ts
// 使用別名
import circleAlias = Shapes.Circle;
import rectAlias = Shapes.Rect;
const circle = new circleAlias(10);
const rect = new rectAlias(4, 6);
console.log("圓形面積:", circle.calculateArea()); // 輸出: 圓形面積: 314
console.log("方形面積:", rect.calculateArea()); // 輸出: 方形面積: 24
編譯後,記得要在 html 加入 shapes.js,不然會出錯誤。
<!-- dist/index.html -->
<!DOCTYPE html>
<!-- ... -->
<body>
<script type="module" src="./app.js"></script>
<script src="shapes.js"></script> <!-- 加入 -->
</body>
</html>
因為目前威爾豬只單純使用 tsc 環境的關係,編譯後還要自己手動加上檔案,有點麻煩。除非我們特別去修改,讓它最後只編譯出一隻 app.js,這樣就不需要再去 html 加入 shapes.js 了。不過這種方式比較不推薦,因為它只支持 AMD 和 SystemJS 模組系统,不適用其他的模組 ( ex: ES6 )。
// tsconfig.json
{
"compilerOptions": {
// ...
"module": "AMD", // AMD 或 System
"outFile": "./dist/app.js", // 指定要編譯出的檔案位置和名稱
// ...
}
}
所以威爾豬改成在 Namespace 使用 Module 的方式 ( ❌ 不要用
):
// shapes.ts
export namespace Shapes {
abstract class Shape {
constructor(protected a: number, protected b?: number) {}
}
export class Circle extends Shape {
calculateArea(): number {
return Math.round(Math.PI * this.a ** 2);
}
}
export class Rect extends Shape {
calculateArea(): number {
return this.a * this.b;
}
}
}
// app.ts
import { Shapes } from "./shapes.js";
const circle = new Shapes.Circle(10);
const rect = new Shapes.Rect(4, 6);
console.log("圓形面積:", circle.calculateArea()); // 輸出: 圓形面積: 314
console.log("方形面積:", rect.calculateArea()); // 輸出: 方形面積: 24
雖然最後在模組上使用命名空間解決了問題,但官方有明確的跟我們說:不要在模組中使用命名空間
,因為命名空間在使用模組時提供的價值非常少 ( 如果有的話 ) 。
所以當我們決定使用模組或命名空間時,應該根據專案來選擇。以下是一些參考因素:
雖然威爾豬自己沒用過 Namespace 的方式,不過除非有特殊需求,否則在新的 TypeScript 專案中 ( 不管大小專案 ),使用 Module 可能會是更好的選擇。